package com.jsftoolkit.base.components;

import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;

import com.jsftoolkit.ajax.AjaxContext;
import com.jsftoolkit.base.ChildManager;
import com.jsftoolkit.base.JsfIterator;
import com.jsftoolkit.base.RequiresResources;
import com.jsftoolkit.base.ResourceConstants;
import com.jsftoolkit.base.ResourceInfo;
import com.jsftoolkit.base.renderer.ResourceUtils;
import com.jsftoolkit.utils.Utils;

/**
 * Depth first searches the component tree for components or components with
 * renderers that implement {@link RequiresResources}. The classpath resources
 * are then included via shale remoting.
 * 
 * @see #processChild(UIComponent, FacesContext, Set)
 * @author noah
 * 
 */
@SuppressWarnings("unchecked")
public class HtmlScripts extends HtmlScriptsBase {

	private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory
			.getLog(HtmlScripts.class);

	private static final String DELIMETER = ";";

	/**
	 * Types of components to skip over, because calling isRendered on their
	 * children is not safe.
	 */
	public static final Set<Class<? extends UIComponent>> SKIP = new HashSet<Class<? extends UIComponent>>();

	static {
		try {
			SKIP.add((Class<? extends UIComponent>) Class
					.forName("com.sun.facelets.component.UIRepeat"));
		} catch (ClassNotFoundException e) {
			// ignore
		}

	}

	@Override
	public void decode(FacesContext context) {
		// process the renderer's decode method (if there is one)
		super.decode(context);

		// parse the list of resources already in the view
		String value = context.getExternalContext().getRequestParameterMap()
				.get(getClientId(context));
		// this reset is important because it clears the saved state
		ResourceUtils.getRenderedResources(context).clear();
		ResourceUtils.getRenderedResources(context).addAll(
				Utils.asSet(Utils.split(value, DELIMETER)));

		// always re-render this component
		AjaxContext ajax = AjaxContext.getCurrentInstance();
		if (ajax.isAjaxRequest(context)) {
			ajax.addComponents(this);
		}
	}

	/**
	 * Calls {@link #processChildren(UIComponent, FacesContext)} on the
	 * {@link UIViewRoot}.
	 * 
	 */
	@Override
	public void encodeEnd(FacesContext context) throws IOException {
		super.encodeEnd(context);
		if (!isRendered()) {
			return;
		}

		// gather the resources to include
		UIViewRoot viewRoot = context.getViewRoot();
		LinkedHashSet<ResourceInfo> resources = new LinkedHashSet<ResourceInfo>();
		processChildren(viewRoot, context, resources);

		Set<String> renderedResources = ResourceUtils
				.getRenderedResources(context);
		// write out all the resource includes.
		ResourceUtils
				.writeIncludes(context, this, resources, renderedResources);

		// write out our id and the already rendered resources
		ResponseWriter writer = context.getResponseWriter();
		writer.startElement("script", this);
		writer.writeAttribute("type", "text/javascript", null);
		writer.writeAttribute("id", getClientId(context), "id");
		writer.writeText("JsfTk.HtmlScriptsId = '", null);
		writer.writeText(getClientId(context), "id");
		writer.writeText("';\nJsfTk.HtmlScriptsResources = '", null);
		writer.writeText(Utils.join(renderedResources.iterator(), DELIMETER),
				null);
		writer.writeText("';", null);
		writer.endElement("script");
	}

	/**
	 * Processes the children of the given component, adding resources to the
	 * passed set.
	 * 
	 * @param component
	 * @param context
	 * @param resources
	 */
	protected static void processChildren(UIComponent component,
			FacesContext context, final LinkedHashSet<ResourceInfo> resources) {
		Iterator<UIComponent> it = component.getFacetsAndChildren();
		while (it.hasNext()) {
			UIComponent c = it.next();
			try {
				processChild(context, c, resources);
			} catch (Exception e) {
				// who knows what weird things some custom components might do
				// that cause exceptions?
				// also, some of the components *could* be in a weird state
				// since we're bluffing our way through the tree walk
				LOG.warn("An Exception (trace logged at debug) "
						+ "was thrown looking for " + "resources in " + c
						+ ".  Probably some of your code "
						+ "is not null tolerant, you may want to fix that.");
				LOG.debug("The aforementioned exception.", e);
			}
		}
	}

	/**
	 * Recursively process the children of the given component, collecting
	 * resources for those rendered components that implement
	 * {@link RequiresResources}. Note that component is not processed in this
	 * invocation.
	 * <p>
	 * If a component's renderer implements {@link ChildManager} or it does not
	 * have a renderer and implements it, its children are not processed.
	 * 
	 * @param component
	 * @param context
	 * @param renderedResources
	 *            a {@link LinkedHashSet} of the resources. It must be a
	 *            {@link LinkedHashSet} because order matters.
	 * @throws IOException
	 */
	public static void processChild(FacesContext context, UIComponent child,
			final LinkedHashSet<ResourceInfo> resources) {
		if (!child.isRendered()) {
			// skip components that are not rendered (which means their children
			// cannot be rendered either) or that have already been rendered
			// (which means their children have as well)
			return;
		}

		// resolve the component's renderer, if it has one
		Renderer renderer = getRenderer(child, context);

		// determine if the component or its renderer implements
		// RequiresResources
		RequiresResources rr = null;
		if (renderer instanceof RequiresResources) {
			rr = (RequiresResources) renderer;
		} else if (child instanceof RequiresResources) {
			// check the component too
			rr = (RequiresResources) child;
		}

		if (rr != null) {
			// add the resources
			resources.addAll(rr.getResources(context, child));
		}

		if (SKIP.contains(child.getClass())) {
			LOG.warn("Skipping " + child + " in resources search. "
					+ "You should use jsfTk:dataIterator instead.");
		} else if (child instanceof UIData) {
			// make an effort to process data tables correctly
			UIDataProcessor.iterate(context, new UIDataProcessor.UIDataWrapper(
					(UIData) child), new UIDataProcessor() {
				@Override
				public void preprocess(FacesContext context, JsfIterator data) {
					for (UIComponent facet : data.getFacets().values()) {
						this.processChild(context, facet);
					}
				}

				@Override
				public void processChild(FacesContext context, UIComponent child) {
					HtmlScripts.processChild(context, child, resources);
				}
			});
		} else if (!(rr instanceof ChildManager)) {
			// process the child's children
			processChildren(child, context, resources);
		}
	}

	/**
	 * Gets a component's renderer.
	 * 
	 * @param c
	 * @param context
	 * @return the renderer for the given component.
	 */
	protected static Renderer getRenderer(UIComponent c, FacesContext context) {
		String family = c.getFamily();
		String rendererType = c.getRendererType();
		if (family != null && rendererType != null) {
			return context.getRenderKit().getRenderer(family, rendererType);
		}
		return null;
	}

	/**
	 * Requires jsfTk.js
	 */
	public Set<ResourceInfo> getResources(FacesContext context,
			UIComponent component) {
		return Utils.asSet(ResourceConstants.JSFTK_JS);
	}

	@Override
	public boolean getRendersChildren() {
		return true;
	}

}
